// TODO: CODE REVIEW & CLEANUP!
//
// Messages.cs
//
// Implements classes for managing messages, each consisting of a series of headers plus an
// arbitrary byte stream.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using HomeUX.Utilities;

namespace HomeUX.Networking
{

/// <summary>
/// TODO
/// </summary>
[DebuggerDisplay("{DebuggerDisplay}")]
public class Message
{
    /// <summary>
    /// This is a special value returned by TODO-StringToObject-???, to indicate a failure condition.
    /// </summary>
    public static readonly FailedValueType FailedValue = new FailedValueType();

    /// <summary>
    /// The type of <r>FailedValue</r>.
    /// </summary>
    [DebuggerDisplay("SyncNodeUpdate Failed")]
    public class FailedValueType : Object
    {
        // the only instance of this class should be the static value
        // SyncNodeUpdate.FailedValue

        /// <summary>
        /// Initializes an instance of this class.
        /// </summary>
        internal FailedValueType()
        {
        }
    }

    /// <summary>
    /// The name of the Content-Length header.
    /// </summary>
    internal const string ContentLengthHeaderName = "Content-Length";

    string _name;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (value != null)
            {
                if (value.Length == 0)
                    throw new ArgumentException("Message.Name may not be String.Empty", "name");
                if (value.Contains("\r\n"))
                    throw new ArgumentException("Message.Name may not contain CRLF", "name");
            }
            _name = value;
        }
    }

    Dictionary<string, string> _headers = new Dictionary<string, string>();

    public byte[] Body { get; set; }

    public Message() :
        this(null, (byte[])null)
    {
    }

    // name may be null, but may not be String.Empty; must be populated before GetBufferList is called
    public Message(string name) :
        this(name, (byte[])null)
    {
    }

    // name may be null, but may not be String.Empty; must be populated before GetBufferList is called
    public Message(string name, string body) :
        this(name, Encoding.UTF8.GetBytes(body))
    {
    }

    // creates a "MessageElements" message containing the message elements that are currently
    // in <builder>
    public Message(StringBuilder builder) :
        this("MessageElements", builder.ToString())
    {
    }

    // name may be null, but may not be String.Empty; must be populated before GetBufferList is called
    public Message(string name, byte[] body)
    {
        Name = name;
		Body = body;
    }

    //public void TransformBody(ICryptoTransform cryptoTransform)
    //{
    //    // transform <_body> using <cryptoTransform>
    //    if (Body != null)
    //        Body = cryptoTransform.TransformFinalBlock(Body, 0, Body.Length);
    //}

    public Message Transform(ICryptoTransform cryptoTransform)
    {
        return new Message()
        {
            _name = _name,
            _headers = _headers,
            Body = (Body != null)
                ? cryptoTransform.TransformFinalBlock(Body, 0, Body.Length) : null
        };
    }

    /// <summary>
    /// Returns an <n>IList</n> of <n>ArraySegment&lt;byte&gt;</n> containing the headers and body
    /// of this message.  A Content-Length header is automatically included, but is not added to
	/// <r>Headers</r>.
    /// </summary>
    ///
	public IList<ArraySegment<byte>> GetBufferList()
	{
        if (Name == null)
            throw new InvalidOperationException("Name has not been set");

		// set <headerBuilder> to be a StringBuilder containing a copy of all headers
		int length = 0; // a high estimate of StringBuilder required size
		const string headerFormat = "{0}: {1}\r\n";
		foreach (KeyValuePair<string,string> header in _headers)
			length += header.Key.Length + header.Value.Length + headerFormat.Length;
        length += Name.Length + headerFormat.Length;
		StringBuilder headerBuilder = new StringBuilder(length);
        headerBuilder.Append(Name);
        headerBuilder.Append("\r\n");
		foreach (KeyValuePair<string,string> header in _headers)
		{
            // skip this header if it's "Content-Length"; for example, if the caller is forwarding
            // a previously-received message -- possibly with altered body and/or headers --
            // "Content-Length" may be present but could be incorrect, and in any case we add it
            // after this loop
            if (header.Key == ContentLengthHeaderName)
                continue;

			// make sure there's no ":" within <header.Key>
			if (header.Key.Contains(":"))
			{
				throw new InvalidOperationException(
					"Colon (:) is not allowed within a header name");
			}

			// make sure there's no CRLF within <header.Value>
			if (header.Value.Contains("\r\n"))
				throw new InvalidOperationException("CRLF is not allowed within a header value");

			// append the header name/value pair
			headerBuilder.AppendFormat(headerFormat, header.Key, header.Value);
		}

		// append a Content-Length header to <headerBuilder>
		headerBuilder.AppendFormat(headerFormat, ContentLengthHeaderName,
			((Body == null) ? 0 : Body.Length));

		// append CRLF <headerBuilder> -- this is the CRLF that separates the headers from the
		// body
		headerBuilder.Append("\r\n");

		// construct and return a buffer list
		var bufferList = new List<ArraySegment<byte>>(2);
        bufferList.Add(new ArraySegment<byte>(Encoding.UTF8.GetBytes(headerBuilder.ToString())));
        if (Body != null)
            bufferList.Add(new ArraySegment<byte>(Body));
		return bufferList;
	}

    /// <summary>
    /// Sets the message headers to a given <n>ArraySegment</n>.  WARNING: Headers are not
    /// encrypted.
    /// </summary>
    ///
    /// <param name="headersSegment">The <n>ArraySegment</n> to set to.</param>
    ///
    public void SetHeaders(ArraySegment<byte> headersSegment)
    {
        SetHeaders(Encoding.UTF8.GetString(headersSegment.Array, headersSegment.Offset,
            headersSegment.Count));
    }

    /// <summary>
    /// Sets the message headers to a given string.  WARNING: Headers are not encrypted.
    /// </summary>
    ///
    /// <param name="headersString">The <n>String</n> to set the headers to.</param>
    ///
    public void SetHeaders(string headersString)
    {
		Name = null;
        _headers.Clear();
        string[] lines = headersString.Split(new char[] { '\r', '\n' },
            StringSplitOptions.RemoveEmptyEntries);
        foreach (string line in lines)
        {
            int index = line.IndexOf(": ");
            if (index < 0)
				Name = line;
            else
                _headers[line.Substring(0, index)] = line.Substring(index + 2);
        }
    }

    /// <summary>
    /// Gets the message headers.  WARNING: Headers are not encrypted.
    /// </summary>
    public IDictionary<string, string> Headers
    {
        get
        {
            return _headers;
        }
    }

    //"raw" body length, i.e. before decryption, if any
    public int ContentLength
    {
        get
        {
            string lengthString;
            if (_headers.TryGetValue(ContentLengthHeaderName, out lengthString))
            {
                int length;
                int.TryParse(lengthString, NumberStyles.Integer, CultureInfo.InvariantCulture, out length);
                return length;
            }
            else
                return 0;
        }
    }

    public string BodyAsString
    {
        get
        {
            if (Body == null)
                return null;
            else
                return Encoding.UTF8.GetString(Body, 0, Body.Length);
        }
        set
        {
            if (value == null)
                Body = null;
            else
                Body = Encoding.UTF8.GetBytes(value);
        }
    }

    // returns an XmlWriter than will append XML fragments to an existing StringBuilder
    public static XmlWriter CreateMessageElementWriter(StringBuilder builder)
    {
        if (builder.Length > 0)
            builder.Append("\r\n");
        var settings = new XmlWriterSettings()
        {
            ConformanceLevel = ConformanceLevel.Fragment,
            Indent = true,
            IndentChars = "\t",
            Encoding = Encoding.UTF8,
            OmitXmlDeclaration = true
        };
        return XmlWriter.Create(builder, settings);
    }

    // calls a given delegate once for each XML element within a the body
    public void EnumerateMessageElements(Action<XmlReader> action)
    {
        if (Body == null)
            throw new InvalidOperationException("Message has no body");
        using (var memoryStream = new MemoryStream(Body, 0, Body.Length, false, false))
        {
            try
            {
                var settings = new XmlReaderSettings()
                {
                    ConformanceLevel = ConformanceLevel.Fragment,
                    IgnoreComments = true,
                    IgnoreProcessingInstructions = true,
                    IgnoreWhitespace = true
                };
                using (XmlReader reader = XmlReader.Create(memoryStream, settings))
                {
                    reader.MoveToContent();
                    while (reader.NodeType == XmlNodeType.Element)
                    {
                        using (XmlReader childReader = reader.ReadSubtree())
                        {
                            if (childReader.Read())
                                action(childReader);
                        }
                        reader.Skip();
                    }
                }
            }
            catch (XmlException ex)
            {
                throw new MessageProtocolException(ex, "Invalid message XML");
            }
        }
    }

    // throws FormatException and OverflowException
    public static object XmlStringToObject(string stringValue, TypeCode typeCode)
    {
        switch (typeCode)
        {
        case (TypeCode)(-1):
            return FailedValue;
        case TypeCode.Empty:
            return null;
        case TypeCode.Boolean:
            return XmlConvert.ToBoolean(stringValue);
	    case TypeCode.Char:
            return XmlConvert.ToChar(stringValue);
	    case TypeCode.SByte:
            return XmlConvert.ToSByte(stringValue);
	    case TypeCode.Byte:
            return XmlConvert.ToByte(stringValue);
	    case TypeCode.Int16:
            return XmlConvert.ToInt16(stringValue);
	    case TypeCode.UInt16:
            return XmlConvert.ToUInt16(stringValue);
	    case TypeCode.Int32:
            return XmlConvert.ToInt32(stringValue);
	    case TypeCode.UInt32:
            return XmlConvert.ToUInt32(stringValue);
	    case TypeCode.Int64:
            return XmlConvert.ToInt64(stringValue);
	    case TypeCode.UInt64:
            return XmlConvert.ToUInt64(stringValue);
	    case TypeCode.Single:
            return XmlConvert.ToSingle(stringValue);
	    case TypeCode.Double:
            return XmlConvert.ToDouble(stringValue);
	    case TypeCode.Decimal:
            return XmlConvert.ToDecimal(stringValue);
	    case TypeCode.DateTime:
            return XmlConvert.ToDateTime(stringValue, XmlDateTimeSerializationMode.RoundtripKind);
	    case TypeCode.String:
		    return stringValue;
        default:
            throw new ArgumentException(String.Format("Cannot convert \"{0}\" to type {1}",
                stringValue, typeCode));
        }
    }

    public static string ObjectToXmlString(object value, out TypeCode typeCode)
    {
        if (value == null)
        {
            typeCode = TypeCode.Empty;
            return String.Empty;
        }

        if (value == FailedValue)
        {
            typeCode = (TypeCode)(-1);
            return String.Empty;
        }

        Type type = value.GetType();
        typeCode = Type.GetTypeCode(type);
        switch (typeCode)
        {
		case TypeCode.Boolean:
			return XmlConvert.ToString((Boolean)value);
		case TypeCode.Char:
			return XmlConvert.ToString((Char)value);
		case TypeCode.SByte:
			return XmlConvert.ToString((SByte)value);
		case TypeCode.Byte:
			return XmlConvert.ToString((Byte)value);
		case TypeCode.Int16:
			return XmlConvert.ToString((Int16)value);
		case TypeCode.UInt16:
			return XmlConvert.ToString((UInt16)value);
		case TypeCode.Int32:
			return XmlConvert.ToString((Int32)value);
		case TypeCode.UInt32:
			return XmlConvert.ToString((UInt32)value);
		case TypeCode.Int64:
			return XmlConvert.ToString((Int64)value);
		case TypeCode.UInt64:
			return XmlConvert.ToString((UInt64)value);
		case TypeCode.Single:
			return XmlConvert.ToString((Single)value);
		case TypeCode.Double:
			return XmlConvert.ToString((Double)value);
		case TypeCode.Decimal:
			return XmlConvert.ToString((Decimal)value);
		case TypeCode.DateTime:
			return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.RoundtripKind);
		case TypeCode.String:
			return (String)value;
        default:
            throw new ArgumentException(String.Format(
                "Cannot convert type \"{0}\" to an XML-format string",
                type), "value");
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
    /// from displaying it in quotes.
    /// </summary>
    object DebuggerDisplay
    {
        get
        {
            return new EncapsulatedString("Message{0}: {1} headers; {2:n0} body bytes",
                ((Name != null) ? String.Format(" \"{0}\"", Name) : String.Empty),
                _headers.Count, ((Body != null) ? Body.Length : 0));
        }
    }
}

/// <summary>
/// TODO
/// </summary>
///
public class MessageBuilder : IDisposable
{
    StringBuilder _buffer;
    XmlWriter _writer;
    EnsureSingleThreaded _ensureSingleThreaded = new EnsureSingleThreaded();

    public XmlWriter Writer
    {
        get
        {
            _ensureSingleThreaded.Verify();
            if (_buffer == null)
                _buffer = new StringBuilder();
            if (_writer == null)
                _writer = Message.CreateMessageElementWriter(_buffer);
            return _writer;
        }
    }

    // if true is returned, then a subsequent call to <r>GetMessage</r> may or may not return a message
    // (i.e. a non-null return value); if false is returned then a subsequent call to <r>GetMessage</r>
    // will definitely return null
    public bool MayHaveMessage
    {
        get
        {
            _ensureSingleThreaded.Verify();
            return (_writer != null);
        }
    }

    // turns the buffer into a message, and clears the buffer; returns null if there's no message
    public Message GetMessage()
    {
        _ensureSingleThreaded.Verify();

        // close <_writer>
        if (_writer == null)
            return null;
        _writer.Close();
        _writer = null;

        // no message if nothing written
        if (_buffer.Length == 0)
            return null;

        // turn <_buffer> into a message, clear the buffer, and return the message
        Message message = new Message(_buffer);
        _buffer.Length = 0;
        return message;
    }

    public void Dispose()
    {
        _ensureSingleThreaded.Verify();
        if (_writer != null)
        {
            _writer.Close();
            _writer = null;
            _buffer = null;
        }
    }

    public void WriteAlertElement(MessageAlertType type, string format, params object[] args)
    {
        _ensureSingleThreaded.Verify();
        Writer.WriteStartElement("Alert");
        switch (type)
        {
        case MessageAlertType.Information:
            Writer.WriteAttributeString("Type", "Information");
            break;
        case MessageAlertType.Warning:
            Writer.WriteAttributeString("Type", "Warning");
            break;
        }
        Writer.WriteAttributeString("Message", Util.Format(format, args));
        Writer.WriteEndElement();
    }
}

public enum MessageAlertType
{
    Information,
    Warning
}

/// <summary>
/// Implements either end of client/server bidirectional communication.  Uses the "HomeUX message
/// protocol", in which messages are transferred in each direction without synchronous
/// acknowledgement.  Each message consists of a message name, headers (name/value pairs), and
/// a body (zero or more bytes).  After an initial handshake (a pair of "Hello" messages in each
/// direction, which <r>MessageEndpoint</r> automatically manages), messages are encrypted if a
/// password is passed to the <r>MessageEndpoint</r> constructor.
/// </summary>
/// 
/// <remarks>
/// <para>
/// Typically a derived class will use sockets or something similar to implement message
/// transport, but <r>MessageEndpoint</r> itself is neutral with respect to that implementation
/// choice.  <r>MessageEndpoint</r> is basically a state machine that performs handshaking and
/// other helper functions.
/// </para>
/// <para>
/// Except for the constructor, this class consists mostly of protected or private methods and
/// properties because each derived class needs to ensure that calls to <r>MessageEndpoint</r>
/// methods are protected from concurrent thread access appropriately.
/// </para>
/// </remarks>
///
public abstract class MessageEndpoint : IDisposable
{
	static readonly byte[] HeaderSeparator =
        new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };

    MessagePipe _pipe = new MessagePipe(1000, 1.5, HeaderSeparator);

    string _password;
    byte[] _mySalt;
    bool _isClient;

    ICryptoTransform _encryptor;
    ICryptoTransform _decryptor;

    Message _incomingMessage;

    Disposer _disposer = new Disposer();

    protected bool IsDisposed { get; private set; }

    //protected Exception Failure { get; private set; }

    //MessageEndpointState _state = MessageEndpointState.WaitingForHello;
    ProcessIncomingMessage _processIncomingMessage;

    protected string Password
    {
        get
        {
            return _password;
        }
        set
        {
            if (_mySalt != null)
            {
                throw new InvalidOperationException(
                    "Cannot set Password property after calling ConnectToServer");
            }
            _password = value;
        }
    }

    public MessageEndpoint()
    {
        _processIncomingMessage = ProcessIncomingMessage_WaitingForHello;
    }

    protected void ConnectToServer()
    {
        if (_isClient)
            throw new InvalidOperationException("ConnectToServer already called");
        _isClient = true;
        var message = new Message("Hello");
        if (_password != null)
            _mySalt = Util.GenerateHalfSalt();
        message.Headers["Salt"] = Util.SaltToString(_mySalt);
        SendRawMessage(message);
    }

    
    // throws: MessageProtocolException (e.g. other endpoint is broken),
    // IncorrectPasswordException (bad password)
    protected void OnDataReceived(byte[] buffer, int bytesReceived)
    {
        _pipe.AppendBytes(buffer, 0, bytesReceived);
        while (_pipe.IsHeaderComplete)
        {
            if (_incomingMessage == null)
            {
                _incomingMessage = new Message();
                _incomingMessage.SetHeaders(_pipe.GetHeader());
            }
            ArraySegment<byte> bodySegment;
            int rawBodyLength = _incomingMessage.ContentLength;

			// if the entire body of the message is not yet available in <_pipe>, quit for now
			// (i.e. wait for the rest of the body to arrive)
            if (!_pipe.TryGetBody(rawBodyLength, out bodySegment))
				break;

			// decrypt the body if it's encrypted
            byte[] body;
            if (bodySegment.Count == 0)
                body = null;
            else
            if (_decryptor != null)
            {
                body = _decryptor.TransformFinalBlock(bodySegment.Array,
                    bodySegment.Offset, bodySegment.Count);
            }
            else
            {
                body = new byte[bodySegment.Count];
                Buffer.BlockCopy(bodySegment.Array, bodySegment.Offset, body, 0, bodySegment.Count);
            }
			//_incomingMessage.SetBody(bodySegment);
            _incomingMessage.Body = body;
			Message message = _incomingMessage;
			_incomingMessage = null;
            //try
            //{
            _processIncomingMessage(message);
                // may throw MessageProtocolException or IncorrectPasswordException
            //}
            //catch (Exception ex)
            //{
            //    if (!(ex is MessageProtocolException) && !(ex is IncorrectPasswordException))
            //        throw;
            //    //Failure = ex;
            //    EndConnection(ex);
            //    throw;
            //}
			_pipe.DeleteMessage(rawBodyLength);
        }
    }

    // note that the body of message may be modified by this method
    protected bool SendMessage(Message message)
    {
        // encrypt <message> if appropriate
        if (_encryptor != null)
            message = message.Transform(_encryptor);

        // send <message> to the other endpoint
        return SendRawMessage(message);
    }

    // don't modify <bufferList> after this call
    protected virtual bool SendRawMessage(Message message)
    {
        throw new NotImplementedException();
    }

    delegate void ProcessIncomingMessage(Message message);

    void ProcessIncomingMessage_WaitingForHello(Message message)
    {
		// we're expecting a "Hello" message
		if (message.Name != "Hello")
			throw new MessageProtocolException(null, "Expected Hello message");

		if (_password != null) // i.e. if this is an encrypted channel
		{
			byte[] otherSalt;
			try
			{
				otherSalt = Util.ParseHalfSaltString(message.Headers["Salt"]);
			}
			catch (KeyNotFoundException ex)
			{
				throw new MessageProtocolException(ex, "Expected Salt header in Hello message");
			}
			catch (FormatException ex)
			{
				throw new MessageProtocolException(ex, "Invalid salt format");
			}
			byte[] salt;
			if (_isClient)
				salt = Util.CombineSalt(_mySalt, otherSalt);
			else
			{
				_mySalt = Util.GenerateHalfSalt();
				salt = Util.CombineSalt(otherSalt, _mySalt);
			}

			var aes = new AesManaged();
			_disposer.Push(aes);
			aes.Key = (new Rfc2898DeriveBytes(_password, salt)).GetBytes(256 / 8);
			aes.IV = salt;

			_encryptor = aes.CreateEncryptor();
			_disposer.Push(_encryptor);

			_decryptor = aes.CreateDecryptor();
			_disposer.Push(_decryptor);
		}

		_processIncomingMessage = OnMessageReceived;

		// the Hello handshake has the server encrypting a test string -- if the client can
		// successfully decrypt the string then the client knows the password is correct; note
		// that the following string is the specific string defined in the protocol and cannot
		// be changed without compiling both client and server components
		const string testString = "The quick brown fox jumps over a lazy dog"; // see above
		if (_isClient)
		{
			if (_decryptor != null)
			{
				if (message.Body == null)
					throw new IncorrectPasswordException();
				byte[] plainText;
				try
				{
					plainText = _decryptor.TransformFinalBlock(message.Body, 0,
						message.Body.Length);
				}
				catch (CryptographicException)
				{
					throw new IncorrectPasswordException();
				}
				string plainTextString = Encoding.UTF8.GetString(plainText, 0,
					plainText.Length);
				if (plainTextString != testString)
					throw new IncorrectPasswordException();
			}
			OnConnected();
		}
		else
		{
			var reply = new Message("Hello");
			if (_mySalt != null)
			{
				reply.Headers["Salt"] = Util.SaltToString(_mySalt);
				reply.BodyAsString = testString;
			}
			SendMessage(reply);
		}
    }

    protected virtual void OnConnected()
    {
    }

    protected virtual void OnMessageReceived(Message message)
    {
        //zz
    }

    // ex is the cause of the connection ending, or null if not caused by an exception
    protected virtual void EndConnection(Exception ex)
    {
        // can't be named CloseConnection -- derived classes use that name
        _encryptor = null;
        _decryptor = null;
        _pipe.Clear();
        _mySalt = null;
        _incomingMessage = null;
        _isClient = false;
        _processIncomingMessage = ProcessIncomingMessage_WaitingForHello;
        _disposer.Dispose(); // yes, it's okay here
    }

    void IDisposable.Dispose()
    {
        if (!IsDisposed)
            EndConnection(null);
        IsDisposed = true;
    }
}

// error in how transmitted information is formatted -- can't be caused by e.g. bad SyncNode path,
// invalid SyncNodeUpdate value, incorrect device name, etc. -- those cause a SyncException
class MessageProtocolException : Exception
{
    public MessageProtocolException(Exception innerException, string format, params object[] args) :
        base(Util.Format(format, args), innerException)
    {
    }
}

class IncorrectPasswordException : Exception
{
    public IncorrectPasswordException()
        : base("The password is incorrect")
    {
    }
}

/// <summary>
/// TODO
/// </summary>
///
public interface IMessageEndpoint
{
    bool IsConnected { get; }
    event EventHandler IsConnectedChanged;
    bool SendMessage(Message message);
    event EventHandler<MessageReceivedEventArgs> MessageReceived;
}

[DebuggerDisplay("MessageReceivedEventArgs")]
public class MessageReceivedEventArgs : EventArgs
{
    public Message Message { get; internal set; }
}

/// <summary>
/// A byte buffer, which is grown as needed, and which is parsed into a sequence of one or more
/// messages, each consisting of a header section followed by a header separator followed by a body
/// section.
/// </summary>
///
[DebuggerDisplay("MessagePipe - {_buffer.Length} bytes")]
public class MessagePipe
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// The encapsulated buffer.
    /// </summary>
    GrowableBuffer _buffer;

    /// <summary>
    /// If <r>IsHeaderComplete</r> is <n>true</n>, <r>_headerSeparatorOffset</r> is the offset
    /// of the <r>HeaderSeparator</r> within <r>_buffer</r>.  If not, then
    /// <r>_headerSeparatorOffset</r> is the next offset to search for the header separator.
    /// </summary>
    int _headerSeparatorOffset;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Properties
    //

    /// <summary>
    /// Gets the sequence of bytes which separate the header from the body within the buffer
    /// contained within this <r>MessagePipe</r> .
    /// </summary>
    public byte[] HeaderSeparator { get; private set; }

    /// <summary>
    /// Returns <n>true</n> if the buffer contains <r>HeaderSeparator</r>.  When this happens, the
    /// buffer contains the complete header of one message, plus a header separator, plus zero or
    /// more bytes of the body of that message.  The buffer may also contain additional message(s).
    /// </summary>
    public bool IsHeaderComplete { get; private set; }

    /// <summary>
    /// Returns the number of bytes that are available in the buffer of this <r>MessagePipe</r>
    /// beyond the header and header separator.  Returns zero if <r>IsHeaderComplete</r> is
    /// <n>false</n>.
    /// </summary>
    public int BodyBytesAvailable
    {
        get
        {
            if (IsHeaderComplete)
                return _buffer.Length - _headerSeparatorOffset - HeaderSeparator.Length;
            else
                return 0;
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Public Methods
    //

    /// <summary>
    /// Initializes an instance of this class.
    /// </summary>
    ///
    /// <param name="initialSize">The initial size, in bytes, to allocate the buffer contained
    /// 	within this <r>MessagePipe</r>.  Must be greater than zero.</param>
    ///
    /// <param name="growBy">If the buffer runs out of space, it grows by this factor.  Must be
    ///     greater than 1.0.</param>
    ///     
    /// <param name="headerSeparator">The sequence of bytes that separate a message header from its
    /// 	body within the buffer contained in this <r>MessagePipe</r>.</param>
    ///
    public MessagePipe(int initialSize, double growBy, byte[] headerSeparator)
    {
        _buffer = new GrowableBuffer(initialSize, growBy);
        HeaderSeparator = headerSeparator;
    }

    /// <summary>
    /// Appends a range of bytes to the buffer contained within this <r>MessagePipe</r>.
    /// </summary>
    ///
    /// <param name="source">The source of bytes to copy to this <r>MessagePipe</r>'s buffer.
    /// 	</param>
    ///
    /// <param name="offset">The offset within <pr>source</pr> at which to start copying.</param>
    ///
    /// <param name="size">The number of bytes to copy.</param>
    ///
    /// <remarks>
    /// After calling <r>AppendBytes</r>, check <r>IsHeaderComplete</r> to see if a complete
    /// header has been been read.
    /// </remarks>
    ///
    public void AppendBytes(byte[] source, int offset, int size)
    {
        // append the source bytes to <_buffer>
        _buffer.AppendBytes(source, offset, size);

        // look for <HeaderSeparator> if we haven't found it yet
        LookForHeaderSeparator();
    }

    /// <summary>
    /// Returns the contents of the header as a byte range.
    /// </summary>
    /// 
    /// <exception cref="InvalidOperationException">
    /// <r>IsHeaderComplete</r> is false.
    /// </exception>
    ///
    public ArraySegment<byte> GetHeader()
    {
        if (!IsHeaderComplete)
            throw new InvalidOperationException("The header is not yet complete");
        ArraySegment<byte> contents = _buffer.Contents;
        return new ArraySegment<byte>(contents.Array, 0, _headerSeparatorOffset);
    }

    /// <summary>
    /// Attempts to retrieve the body of the message, given the size that the body is supposed to
    /// be.
    /// </summary>
    ///
    /// <param name="size">The number of bytes that are supposed to be in the message body.</param>
    ///
    /// <param name="body">On success, this references the bytes of the message body.</param>
    ///
    /// <returns>
    /// If the buffer contained within this <r>MessagePipe</r> contains at least <pr>size</pr>
    /// bytes following the header separator, then a byte range referencing those first
    /// <pr>size</pr> bytes are stored in <pr>body</pr> and <n>true</n> is returned.  Otherwise,
    /// <n>false</n> is returned.
    /// </returns>
    /// 
    /// <exception cref="InvalidOperationException">
    /// <r>IsHeaderComplete</r> is false.
    /// </exception>
    ///
    public bool TryGetBody(int size, out ArraySegment<byte> body)
    {
        if (!IsHeaderComplete)
            throw new InvalidOperationException("The header is not yet complete");
        ArraySegment<byte> contents = _buffer.Contents;
        if (contents.Count - _headerSeparatorOffset - HeaderSeparator.Length >= size)
        {
            body = new ArraySegment<byte>(contents.Array,
                _headerSeparatorOffset + HeaderSeparator.Length, size);
            return true;
        }
        else
        {
            body = default(ArraySegment<byte>);
            return false;
        }
    }

    /// <summary>
    /// Removes the message that's at the beginning of the message pipe -- including the message
    /// header, separator, and body.  The remaining bytes in the message pipe buffer are shifted
    /// to the beginning of the buffer, and the state of this object is updated accordingly to
    /// reflect whether or not a complete header is now available for the next message.
    /// </summary>
    ///
    /// <param name="size">The size of the message body, in bytes.</param>
    /// 
    /// <exception cref="InvalidOperationException">
    /// <r>IsHeaderComplete</r> is false, or the message does not contain <pr>size</pr> bytes
    /// beyond the header separator.
    /// </exception>
    ///
    public void DeleteMessage(int size)
    {
        // set <body> to information about the message body
        ArraySegment<byte> body;
        if (!TryGetBody(size, out body))
            throw new InvalidOperationException("The body is not yet complete");

        // delete the entire message
        _buffer.DeleteBytes(0, body.Offset + body.Count);

        // update the state of this object
        IsHeaderComplete = false;
        _headerSeparatorOffset = 0;
        LookForHeaderSeparator();
    }

    /// <summary>
    /// Discards the contents of the buffer contained within this <r>MessagePipe</r>.  The buffer
    /// memory is not deallocated -- it will be used by subsequent <r>AppendBytes</r> operations.
    /// </summary>
    ///
    public void Clear()
    {
        // reset the state of this object
        _buffer.Clear();
        IsHeaderComplete = false;
        _headerSeparatorOffset = 0;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Methods
    //

    /// <summary>
    /// Searches the buffer for <r>HeaderSeparator</r>, and updates object state appropriately.
    /// Does nothing if <r>IsHeaderComplete</r> is <n>true</n>.
    /// </summary>
    ///
    void LookForHeaderSeparator()
    {
        if (!IsHeaderComplete)
        {
            ArraySegment<byte> contents = _buffer.Contents;
            int lastOffset = contents.Count - HeaderSeparator.Length;
            while (_headerSeparatorOffset <= lastOffset)
            {
                if (AreBytesEqual(contents.Array, _headerSeparatorOffset, HeaderSeparator))
                {
                    IsHeaderComplete = true;
                    break;
                }
                _headerSeparatorOffset++;
            }
        }
    }

    /// <summary>
    /// Returns <n>true</n> if one buffer contains the bytes of another at a given offset.
    /// </summary>
    ///
    /// <param name="buffer1">The first buffer.</param>
    ///
    /// <param name="offset1">The offset within the first buffer.</param>
    ///
    /// <param name="buffer2">The second buffer.</param>
    ///
    /// <returns>
    /// <n>true</n> if <pr>buffer1</pr> contains the bytes of <pr>buffer2</pr> at offset
    /// <pr>offset1</pr> within <pr>buffer1</pr>.
    /// </returns>
    ///
    bool AreBytesEqual(byte[] buffer1, int offset1, byte[] buffer2)
    {
        int tailLength = buffer1.Length - offset1;
        if (tailLength < buffer2.Length)
            return false;
        for (int offset2 = 0; offset2 < buffer2.Length; offset2++)
        {
            if (buffer1[offset1++] != buffer2[offset2])
                return false;
        }
        return true;
    }
}

}
